﻿#pragma once

#include "../../src/util/string_util.hh"
#include "color.hh"
#include "fixture.hh"
#include "testcase.hh"
#include <chrono>
#include <ctime>
#include <initializer_list>
#include <iostream>
#include <list>
#include <sstream>
#include <string>
#include <thread>
#include <typeinfo>

namespace k {
namespace test {

// 单元测试框架
class Unittest {
  using FixtureList = std::list<Fixture *>;
  FixtureList fixtureList_;   // 用例集合列表
  static Unittest *unittest_; // 单件
  bool debugBreak_{false};

public:
  // 取得框架指针
  inline static Unittest *unittest() {
    if (!unittest_) {
      unittest_ = new Unittest();
    }
    return unittest_;
  }

public:
  // 构造
  Unittest();
  // 析构
  ~Unittest();
  // 添加用例集合
  bool addFixture(Fixture *fixture);
  // 取得用例集合
  // @param name 名称
  Fixture *getFixture(const std::string &name);
  // 禁用集合
  // @param name 名称
  void disableFixture(const std::string &name);
  // 允许运行集合
  // @param name 名称
  void enableFixture(const std::string &name);
  // 运行所有集合
  void runAllFixture();
  // 运行所有集合，某几个除外
  void runAllFixtureExcept(std::initializer_list<std::string> list);
  // 运行集合
  void runFixture(const std::string &name, std::size_t count = 1);
  // 等待用户按键
  void waitKeyPress();
  // 获取所有用例集合列表
  const FixtureList &getAllFixture();
  // 设置DEBUG BREAK
  void setDebugBreak();
  // 检测DEBUG BREAK
  bool isDebugBreak();
};

} // namespace test
} // namespace k

#define UnittestRef (*k::test::Unittest::unittest())

#if defined(WIN32) && defined(_DEBUG)
inline void triggerDebugBreak() {
  if (UnittestRef.isDebugBreak()) {
    DebugBreak();
  }
}
#else
inline void triggerDebugBreak() { return; }
#endif

#define FIXTURE_BEGIN(name)                                                    \
  namespace _namespace_##name {                                                \
    static k::test::Fixture *_fixture =                                        \
        new k::test::Fixture(std::string(#name));                              \
    static bool _fixture_add = UnittestRef.addFixture(_fixture);

#define FIXTURE_END(name) }

#define SETUP(method)                                                          \
  static bool _fixture_setup_add = _fixture->onSetup(method);
#define TEARDOWN(method)                                                       \
  static bool _fixture_teardown_add = _fixture->onTearDown(method);

#define CASE(name)                                                             \
  class _case_##name : public k::test::Testcase {                              \
    bool stdexcept_;                                                           \
    bool except_;                                                              \
                                                                               \
  public:                                                                      \
    _case_##name(const std::string &name)                                      \
        : k::test::Testcase(#name), stdexcept_(false), except_(false) {}       \
    virtual ~_case_##name() = default;                                         \
    virtual void run() override;                                               \
  };                                                                           \
  static bool _testcase_##name##_add =                                         \
      _fixture->addTestcase(new _case_##name(#name));                          \
  void _case_##name::run()

#define SDK_CASE(name)                                                         \
  class _case_##name : public k::test::Testcase {                              \
    bool stdexcept_{false};                                                    \
    bool except_{false};                                                       \
    bool running_{true};                                                       \
    bool exec_{false};                                                         \
    std::time_t wait_ms_{0};                                                   \
    inline bool is_running() { return running_; }                              \
    inline void quit_case() { running_ = false; }                              \
    inline bool is_exec() { return exec_; }                                    \
    inline void exec() { exec_ = true; }                                       \
    inline bool is_timeout() {                                                 \
      if (_sdk_ptr_) {                                                         \
        return false;                                                          \
      }                                                                        \
      wait_ms_ += 1;                                                           \
      if (wait_ms_ > 5000) {                                                   \
        return true;                                                           \
      } else {                                                                 \
        return false;                                                          \
      }                                                                        \
    }                                                                          \
    inline void clear_timeout() { wait_ms_ = 0; }                              \
                                                                               \
  public:                                                                      \
    _case_##name(const std::string &name) : k::test::Testcase(#name) {}        \
    virtual ~_case_##name() = default;                                         \
    virtual void run() override;                                               \
    void _run_();                                                              \
  };                                                                           \
  static bool _sdk_testcase_##name##_add =                                     \
      _fixture->addTestcase(new _case_##name(#name));                          \
  void _case_##name::run() {                                                   \
    std::time_t counter = 0;                                                   \
    while (is_running()) {                                                     \
      if (is_timeout()) {                                                      \
        get_sdk()->write_log("Connect remote service timeout");                \
        throw std::runtime_error("Connect remote service timeout");            \
      }                                                                        \
      if (!is_exec()) {                                                        \
        if (get_prx()) {                                                       \
          _run_();                                                             \
          clear_timeout();                                                     \
          exec();                                                              \
        }                                                                      \
      }                                                                        \
      get_sdk()->tick();                                                       \
      std::this_thread::sleep_for(std::chrono::milliseconds(1));               \
      counter += 1;                                                            \
      if (counter > 500) {                                                     \
        quit_case();                                                           \
      }                                                                        \
      if (!running_) {                                                         \
        if (isSuccess()) {                                                     \
          get_sdk()->write_log(#name " [PASS]");                               \
        } else {                                                               \
          get_sdk()->write_log(#name " [FAIL]");                               \
          for (const auto &err : getErrorList()) {                             \
            get_sdk()->write_log(err);                                         \
          }                                                                    \
        }                                                                      \
      }                                                                        \
    }                                                                          \
  }                                                                            \
  void _case_##name::_run_()

#define ASSERT_TRUE(expr)                                                      \
  if (!(expr)) {                                                               \
    std::stringstream error;                                                   \
    error << __FILE__ << ":" << __func__ << ":" << __LINE__ << ":"             \
          << std::endl                                                         \
          << "    > " << #expr;                                                \
    setError(error.str());                                                     \
    triggerDebugBreak();                                                       \
  }

#define ASSERT_FALSE(expr) ASSERT_TRUE(!(expr))

#define ASSERT_EQUAL(a, b) ASSERT_TRUE((a) == (b))

#define ASSERT_NEQUAL(a, b) ASSERT_TRUE((a) != (b))

#define ASSERT_NULL(expr) ASSERT_TRUE((a) == nullptr)

#define ASSERT_STDEXCEPT(expr)                                                 \
  stdexcept_ = false;                                                          \
  try {                                                                        \
    expr;                                                                      \
  } catch (std::exception & e) {                                               \
    std::cout << "Exception type ["                                            \
              << kratos::util::demangle(typeid(e).name()) << "]\n"             \
              << e.what() << std::endl;                                        \
    stdexcept_ = true;                                                         \
  }                                                                            \
  ASSERT_TRUE(stdexcept_);                                                     \
  stdexcept_ = false;

#define ASSERT_EXCEPT(expr)                                                    \
  except_ = false;                                                             \
  try {                                                                        \
    expr;                                                                      \
  } catch (std::exception & e) {                                               \
    std::cout << "Exception type ["                                            \
              << kratos::util::demangle(typeid(e).name()) << "]\n"             \
              << e.what() << std::endl;                                        \
    except_ = true;                                                            \
  }                                                                            \
  ASSERT_TRUE(except_);                                                        \
  except_ = false;

#define ASSERT_NOEXCEPT(expr)                                                  \
  except_ = false;                                                             \
  try {                                                                        \
    expr;                                                                      \
  } catch (...) {                                                              \
    except_ = true;                                                            \
  }                                                                            \
  ASSERT_TRUE(!except_);                                                       \
  except_ = false;

inline bool check_sleep(bool &b, std::time_t maxMs) {
  std::time_t c = 0;
  while (!b) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    c += 100;
    if (c > maxMs) {
      return b;
    }
  }
  return b;
}

inline bool check_sleep(bool &p1, bool &p2, std::time_t maxMs) {
  std::time_t c = 0;
  while (!p1 || !p2) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    c += 100;
    if (c > maxMs) {
      return p1 && p2;
    }
  }
  return p1 && p2;
}

inline bool check_sleep(bool &p1, bool &p2, bool &p3, std::time_t maxMs) {
  std::time_t c = 0;
  while (!p1 || !p2 || !p3) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    c += 100;
    if (c > maxMs) {
      return p1 && p2;
    }
  }
  return p1 && p2 && p3;
}

#ifdef _WIN32
constexpr static auto *_so_path_ =
    "..\\..\\..\\src\\sdk\\bin\\Debug\\libsdk_frame.so";
#else
constexpr static auto *_so_path_ = "../../../src/sdk/bin/libsdk_frame.so";
#endif

#define SDK_TEST_DECLARE(name)                                                 \
  name##Proxy *_case_prx_ = nullptr;                                           \
  BoxSDK *_sdk_ptr_ = nullptr;                                                 \
  std::string _error_;                                                         \
  inline BoxSDK *get_sdk() { return _sdk_ptr_; }                               \
  inline name##Proxy *get_prx() { return _case_prx_; }                         \
  SETUP([]() {                                                                 \
    _sdk_ptr_ = load_and_get_frame(_so_path_);                                 \
    if (!_sdk_ptr_) {                                                          \
      throw std::runtime_error("libsdk_frame.so not found");                   \
    }                                                                          \
    _sdk_ptr_->on_connected([&](BoxSDK &) {                                    \
      _case_prx_ = _sdk_ptr_->get_proxy<name##Proxy>(_error_);                 \
    });                                                                        \
    _sdk_ptr_->on_disconnected([&](BoxSDK &) {});                              \
    _sdk_ptr_->initialize("sdk.cfg", _error_);                                 \
    _sdk_ptr_->write_log("run [" #name "]");                                   \
  })                                                                           \
  TEARDOWN([]() {                                                              \
    if (_sdk_ptr_) {                                                           \
      _sdk_ptr_->write_log("run [" #name "] finish");                          \
      _sdk_ptr_->deinitialize();                                               \
    }                                                                          \
  })
